chore(plugin): lead both manifests with skill evaluation, add discovery metadata - #141
Conversation
|
Claude finished @uipreliga's task in 1m 41s —— View job 📋 Review Task List
|
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:141 (2 files) axis:1,2,3,4,5,6,7,8
Scope: pr:141 (2 files) axis:1,2,3,4,5,6,7,8 · branch chore/plugin-discovery-metadata · c0156b4 · 2026-08-27T19:53Z · workflow variant
Change class: trivial — plugin/marketplace manifest metadata only (descriptions, keywords, $schema, discovery fields); no Python, control flow, or persisted-schema semantics touched
The harness core is in excellent shape — perfect scores on type safety, tests, security, architecture, error handling, API surface, and evaluation-harness quality, with zero critical/high/medium findings and nothing that can change a task's score or final_status for identical agent output — so the only real risk is editorial drift in the newly duplicated plugin discovery metadata (keywords vs tags split across two manifests with no rule and no parity test), which costs nothing today but rots quietly; bottom line: ship it, and spend the small remaining effort on making the two manifests a single enforced surface.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.8 / 10 | 0 | 0 | 0 | 2 | tags added alongside keywords on the marketplace entry: 12 discovery strings split across two synonymous fields with no rule, disjoint values, and no counterpart in plugin.json |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 7. API Surface & Maintainability | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 8. Evaluation Harness Quality | 10 / 10 | 0 | 0 | 0 | 0 | — |
Overall Score: 10 / 10 · Weakest Axis: Code Quality & Style at 9.8 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 0 · 🔵 2 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
None.
Nits
- [Axis 1]
tagsadded alongsidekeywordson the marketplace entry: 12 discovery strings split across two synonymous fields with no rule, disjoint values, and no counterpart in plugin.json (.claude-plugin/marketplace.json:29) — Line 29 adds"tags": ["skills", "evals", "github-actions", "sandbox", "llm-judge"]directly under the 7-item"keywords"block (lines 20-28). The pinned schema (fetched from https://www.schemastore.org/claude-code-marketplace.json) describes the two fields near-identically —keywords: 'Tags for plugin discovery and categorization';tags: 'Tags for searchability and discovery' — so there is no consumer distinction to key the split on, and the two value sets are fully disjoint (set(tags) & set(keywords) == set()) while containing synonyms across the boundary:evals/evaluation, andskillsagainst three keyword variants of the same concept (claude-code-skills,skill-testing,skill-activation). The asymmetry is the concrete cost: the plugin-manifest schema has notagsproperty at all (top-level props are$schema, agents, author, channels, commands, dependencies, description, homepage, hooks, keywords, license, lspServers, mcpServers, monitors, name, outputStyles, repository, settings, skills, themes, userConfig, version), so plugins/coder-eval/.claude-plugin/plugin.json cannot carry the fivetagsvalues — an installed user's manifest loses them, and a future editor has no rule for whether a new term belongs inkeywordsortags. Recommendation: droptagsand fold any genuinely distinct terms into the singlekeywordslist that both manifests share (which also removes five more strings from the parity surface in finding #1), or add a one-line comment/doc note stating the split rule if both are kept deliberately. - [Axis 1] Newly pinned
$schemadoes not definedisplayName, the other key added in the same commit, and nothing validates either file against the pin (plugins/coder-eval/.claude-plugin/plugin.json:4) — The same commit adds"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json"(line 2) and"displayName": "Coder Eval",(line 4) — but I fetched that schema (301 → https://www.schemastore.org/claude-code-plugin-manifest.json,$comment: Generated on 2026-04-23) anddisplayNameis not among itsproperties; same for the marketplace item schema, where.claude-plugin/marketplace.json:12addsdisplayNameto the plugin entry. Because neither schema setsadditionalProperties, both files validate with 0 errors (confirmed withjsonschema.Draft7Validator), so the field is tolerated but uncovered: an editor gives no completion or hover doc for it, and a typo likedisplyNamewould also pass the pinned schema silently. The pin is also purely editorial —.github/workflows/pr-checks.yml:241,244gates onclaude plugin validate --strict, which is stricter than the schema (I verified locally that it emitsUnknown field 'totallyBogusKey'. Claude Code ignores it at load time.and exits 1, while acceptingdisplayName), and no workflow or Makefile target referencesjsonschema/check-jsonschema/ajv/schemastore. Recommendation: keep the pin for editor UX but note in the PR that enforcement stays withclaude plugin validate --strict, and either raise a schemastore PR addingdisplayNameor drop the field until the pinned schema covers it — otherwise the pin implies validation coverage it does not give for the very key added beside it.
What's Missing
Parallel paths:
- 🟡 🟡 Both manifests were re-led with "Test whether your Claude Code skills actually trigger — then author, run, and analyze…", but every prose surface a user actually lands on still leads with the inverted old order: plugins/coder-eval/README.md:3 ("Author, run, and analyze Coder Eval suites from inside Claude Code — including whether your own Claude Code skills actually trigger"), docs/PLUGIN.md:2-5 (frontmatter description), and README.md:100-113 (§ Use inside Claude Code). The plugin README is the page the marketplace one-liner leads to, so the new positioning survives exactly one click. pyproject.toml:4 (the PyPI one-liner) is a fourth copy, also unchanged — decide whether it is deliberately audience-specific or also stale. (trigger: plugins/coder-eval/.claude-plugin/plugin.json)
Tests:
- 🟡 🟡 The commit makes the two manifests carry six byte-identical fields (description, keywords, author, homepage, repository, license) and nothing keeps them in parity: grep over tests/, Makefile and .github/workflows/ finds
.claude-pluginreferenced only by tests/test_action_version_pin.py (version) and tests/test_verify_published_workflow.py (a release-step name), and no test or CE rule mentionskeywords/descriptionfor either file. The repo already institutionalizes exactly this shape forversion(a derived pin with an anchor test); the new duplicated block ships with none. A CE-style test assertingmarketplace.plugins[0][k] == plugin.json[k]for the shared keys is ~10 lines and would make the next one-sided edit fail inmake lint. (trigger: .claude-plugin/marketplace.json) - 🔵 🔵 No test or CI step validates either JSON against the
$schemait now declares — nojsonschema/check-jsonschema/ajvreference exists in the repo, so the pin is editor-only. A pytest that loads both files and validates them against a vendored copy of the two schemas (or an offline JSON-parse + required-key assertion) would make the pin load-bearing and would not depend on network or on the Claude CLI. (trigger: plugins/coder-eval/.claude-plugin/plugin.json) (restates: Axis 1: Newly pinned$schemadoes not definedisplayName)
Downstream consumers:
- 🔵 🔵 The sole gate on the newly added keys is
claude plugin validate --strictin .github/workflows/pr-checks.yml:240-244, whose validator is installed unpinned (npm install -g @anthropic-ai/claude-code, line 233).displayName(both files) andtags(marketplace) are accepted by today's CLI; whichever CLI ships on a future run decides whether they still are, so a metadata-only change can turn an unrelated PR red with no repo-side signal. Pin the CLI version (or add the offline schema test above) so the acceptance criterion for these keys is versioned. (trigger: .claude-plugin/marketplace.json)
Display & mapping dicts:
- 🔵 🔵 The generated docs-index blurb for the plugin page still reads "Install the Claude Code plugin — author, run, and analyze suites from inside the agent" — mkdocs.yml:92 (the SSOT) mirrored into README.md:218, docs/index.md:90 and docs/llms.txt:37. CE028 only enforces that the three generated tables match mkdocs.yml, so it cannot see that the blurb now contradicts the manifests; updating means editing mkdocs.yml then running
make docs-indexes(never hand-editing the tables). (trigger: plugins/coder-eval/.claude-plugin/plugin.json)
Daily/nightly:
- 🔵 🔵 Rollout skew is unstated. marketplace.json is served unversioned from main, so
/plugin marketplace addbrowsers see the new description immediately; plugin.json'sversionis a derived pin of pyproject (0.11.3, correctly untouched here and guarded by tests/test_action_version_pin.py), and per that test's own docstring Claude Code keys plugin updates off it — so already-installed users keep the old description/keywords until the next release. The PR should say the plugin-side half of this metadata goes live only at the next release, which is also the window in which the two duplicated descriptions are observably different in the wild. (trigger: plugins/coder-eval/.claude-plugin/plugin.json) - 🔵 🔵 No post-release or nightly job covers the plugin manifests. plugin-validate lives in pr-checks.yml (pull_request/push/merge_group only), while .github/workflows/verify-published-action.yml — the nightly + post-Release parity job — verifies the GitHub Actions Marketplace listing, tag, pin and PyPI, and never touches
/plugin marketplace add UiPath/coder_eval. This change expands the plugin's public discovery surface without extending that verification, so a manifest that stops installing after a release is only noticed by users. (trigger: .claude-plugin/marketplace.json)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] New rule CE044 — plugin/marketplace manifest metadata parity (next free number; highest in tree is CE043). Statement: the marketplace plugin entry and the plugin manifest it points at must carry identical values for every field both manifests can express, and the marketplace entry must not carry a discovery field the plugin manifest cannot mirror. Implementation: a whole-tree JSON rule (not a per-file
.pyASTBaseRule), so wire it like CE026/CE028/CE033 as a dedicated@pytest.mark.linttest class intests/test_custom_lint.pybacked by a helper moduletests/lint/plugin_manifest_parity.py. It (a) parses/Users/religa/src/coder_eval/.claude-plugin/marketplace.json, resolves each entry'ssource(./plugins/coder-eval) to a real directory containing.claude-plugin/plugin.json(fails if it does not resolve), (b) asserts byte-equality of the shared keysname,description,author,homepage,repository,license,keywords, and (c) fails on any extra discovery-string key on the marketplace entry (tags, and anything future) unless it is listed in an explicitMARKETPLACE_ONLY = {"key": "reason"}allowlist in the helper — the allowlist entry is the missing "rule for where a new term goes", recorded in code instead of tribal knowledge. Prevents: Finding #1 directly:"tags": ["skills", "evals", "github-actions", "sandbox", "llm-judge"]at.claude-plugin/marketplace.json:29has no counterpart property in the plugin manifest, so those five discovery strings are dropped from an installed user's copy, and the split against the 7-itemkeywordsblock (lines 20-28) has no stated rule while holding cross-boundary synonyms (evals/evaluation,skills/claude-code-skills). It also prevents the untested drift class the same shape invites: editingdescription/keywords/homepagein one manifest only — today nothing intests/compares the two files (onlytests/test_action_version_pin.pytouchesplugin.json, and only itsversion). - [ce-lint] New rule CE045 — a pinned
$schemamust actually be enforced, closed-world. Statement: every JSON file in the repo that declares a$schemamust validate against a vendored copy of that exact schema, and every key it uses (top-level, plus eachplugins[]entry for the marketplace file) must be present in that schema'spropertiesunless allowlisted with a reason. Implementation: vendor the two documents astests/lint/schemas/claude-code-plugin-manifest.jsonandtests/lint/schemas/claude-code-marketplace.json, then add a@pytest.mark.lintclass over a helpertests/lint/schema_pin_coverage.pythat runsjsonschema(already a runtime dep,pyproject.toml:50— no new dependency, no network) and then the extra closed-world key check, withUNCOVERED_KEYS = {("plugin.json", "displayName"): "<reason>"}. The closed-world half is the load-bearing part: neither schemastore document setsadditionalProperties, so plain validation returns 0 errors for any key. Prevents: Finding #2:plugins/coder-eval/.claude-plugin/plugin.json:2pinshttps://json.schemastore.org/claude-code-plugin-manifest.jsonwhile line 4 adds"displayName": "Coder Eval", which that schema does not define (same fordisplayNameon the marketplace entry at.claude-plugin/marketplace.json:12) — and nothing inMakefile,tests/, or.github/workflows/validates either file against the pin, so both the uncovered key and a typo likedisplyNamepass silently. Under CE045 the uncovered key becomes a lint failure that must be either dropped or allowlisted with a written reason, and a misspelling of any covered key fails outright without waiting for the CI-onlyclaude plugin validate --strictstep.
Harness improvements (not statically reachable):
- Vendor the two schemastore documents under
tests/lint/schemas/(the prerequisite CE045 needs to run offline) and add amake plugin-schemas-refreshtarget plus a scheduled workflow job that re-fetches both URLs and fails if the vendored copy is stale. Note in the target thatjson.schemastore.org301-redirects towww.schemastore.organd that the plugin-manifest document is dated ($comment: Generated on 2026-04-23), so "the pin is current" is a fact with an expiry date. Why not static: Freshness of an externally versioned document can only be established by fetching it. Themake lintgate must stay hermetic and offline (it runs in everymake verify), so the network half has to be a scheduled job while the lint half reads the vendored snapshot. Prevents: Finding #2's underlying gap — the pin implies coverage it does not give. When upstream addsdisplayName(or the team raises the schemastore PR), the refresh job is what tells us the allowlist entry can be deleted, instead of the exemption calcifying. - Pin the validator in the
plugin-validatejob:.github/workflows/pr-checks.yml:238runs an unpinnednpm install -g @anthropic-ai/claude-code(as do 6 other jobs), so the strictness of the only real manifest gate floats with whatever the registry serves that morning. Pin an exact version (renovate/dependabot-bumped) and record in the job comment thatclaude plugin validate --strict— not the$schemapin — is the enforcement authority for these two files. Why not static: The gate's strength is a property of an external binary's runtime behavior; no AST or schema check can tell whether the installed CLI still rejects an unknown key. Prevents: Finding #2's expectation mismatch (the newly added$schemapin reads as validation coverage while the actual gate lives in CI), and the silent weakening of that gate across CLI releases. - Add a canary step to the same
plugin-validatejob: copyplugins/coder-eval/.claude-plugin/plugin.jsonto a temp dir, inject a bogus key, and assertclaude plugin validate --strictexits non-zero. Today the job only proves the good manifests pass, which is also what a validator that silently stopped enforcing--strictwould report. Why not static: Requires executing the external CLI against a mutated fixture; the assertion is about observed exit status, not about repository text. Prevents: Silent regression of the unknown-field / typo class that finding #2 rests on (displyNamepassing unnoticed), and by extension thetags-style unmirrored-field class from finding #1 if a future CLI version starts flagging it.
Top 5 Priority Actions
- No finding affects scoring or final_status, so start with the only structural gap: collapse the synonymous
tagslist at .claude-plugin/marketplace.json:29 into the singlekeywordsblock above it (lines 20-28), since the values are fully disjoint yet semantically overlapping (evals/evaluation,skillsvs three keyword variants) andtagshas no counterpart property in the plugin-manifest schema, so an installed user's copy silently loses those five terms. - Add a parity test (mirroring tests/test_action_version_pin.py) asserting that the shared discovery fields —
description,keywords,author,homepage,repository,license— are byte-identical between .claude-plugin/marketplace.json:14-29 and plugins/coder-eval/.claude-plugin/plugin.json:6-19, because nothing today catches a one-sided edit to either file. - Decide the fate of
displayNameat plugins/coder-eval/.claude-plugin/plugin.json:4 (and .claude-plugin/marketplace.json:12): it is absent from both pinned schemas, so an editor gives no completion and a typo likedisplyNamevalidates clean — either raise a schemastore PR adding it or drop the key until the pin covers it. - Record in the PR description (or a comment near plugins/coder-eval/.claude-plugin/plugin.json:2) that the new
$schemapins are editorial-only and that real enforcement stays withclaude plugin validate --strictat .github/workflows/pr-checks.yml:241,244, so a future reader does not assume the pin gates CI. - If both
keywordsandtagsare kept deliberately, write the one-line split rule into CLAUDE.md's plugin section so the next editor has a deterministic place to put a new discovery term — otherwise the two lists diverge on the next skill added.
Stats: 0 🔴 · 0 🟠 · 0 🟡 · 2 🔵 across 8 axes reviewed.
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:141
Scope: pr:141 · branch chore/plugin-discovery-metadata · c0156b4 · 2026-08-27T20:09Z · workflow variant
Change class: trivial — metadata/manifest-only edit: two JSON discovery manifests (description text, keywords/tags, displayName, $schema pins); no Python, no code path, no schema consumed by the harness
The change is safe and well scoped — nothing here touches the harness, so no task's score or final_status can move for identical agent output (Security, Architecture, Error Handling and Evaluation Harness are all clean at 10/10) — and the only real risk is maintenance drift in the plugin's public shopfront, where eight metadata fields are now hand-synced byte-for-byte across two manifests with no parity guard (the pair already drifted on keywords on main), the repositioned lede was not rippled to the README and docs surfaces that mirror it, and two new $schema pins declare a contract no test checks; bottom line: ship it, then add the ~15-line parity test the repo's own test_action_version_pin.py precedent calls for.
Anchor note. This review was scoped at
c0156b4. While it ran, the PR
advanced two commits to78de45e(chore(plugin): give the marketplace owner a contact address,chore(plugin): give both author objects the same contact address). Those commits only add"email": "coder-eval@uipath.com"
to the marketplaceownerand to bothauthorobjects — no finding below is
invalidated, and finding 1 is strengthened:authoris now a 9th
byte-identical hand-synced field across the two manifests. Allfile:line
citations below have been re-anchored to78de45e.Coverage caveat. Three sub-agents hit a session limit. Axis 2 (Type
Safety) produced no results and is excluded from the report and from the
Overall mean — read its absence as not reviewed, not as clean. The verify
agents for Axes 5 and 7 also failed; their surviving medium+ finding was
merged into finding 1, whose verifier did run. Overall is the mean over the
7 axes that reported.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.5 / 10 | 0 | 0 | 1 | 0 | Manifest metadata duplicated verbatim across marketplace.json and plugin.json with no parity guard (widened from 3 to 8 shared keys; the pair already drifted on keywords) |
| 3. Test Health | 9.9 / 10 | 0 | 0 | 0 | 1 | Newly pinned $schema URLs are unverified by any test or CI step, and already disagree with the runtime validator over the displayName key this PR adds |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 7. API Surface & Maintainability | 9.7 / 10 | 0 | 0 | 0 | 3 | Newly added keywords/tags discovery vocabulary adds no discovery value — inert for the client search, and internally redundant (near-synonyms, second vocabulary with no plugin.json counterpart) |
| 8. Evaluation Harness Quality | 10 / 10 | 0 | 0 | 0 | 0 | — |
Overall Score: 9.9 / 10 · Weakest Axis: Code Quality & Style at 9.5 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 1 · 🔵 4 across 7 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 1] Manifest metadata duplicated verbatim across marketplace.json and plugin.json with no parity guard (widened from 3 to 8 shared keys; the pair already drifted on
keywords) (.claude-plugin/marketplace.json:15) — The PR grows the hand-synced surface between.claude-plugin/marketplace.json's plugin entry andplugins/coder-eval/.claude-plugin/plugin.jsonfrom 3 fields to 8. Verified byte-identical after this change:displayName(marketplace.json:13 / plugin.json:4),description(marketplace.json:15 / plugin.json:6 — both exactly"Test whether your Claude Code skills actually trigger — then author, run, and analyze the eval suite that proves it, locally or as a CI gate.", 141 chars),author(:16 / :7),homepage(:17 / :8),repository(:18 / :9),license(:19 / :10),keywords(:21-29 / :11-19), plusname.
This is not a hypothetical drift risk — it already happened on main: git show origin/main:.claude-plugin/marketplace.json has "keywords": ["evaluation", "testing", "skills", "benchmark", "ci"] while git show origin/main:plugins/coder-eval/.claude-plugin/plugin.json has ["evaluation", "testing", "claude-code-skills", "benchmark", "ci"]. This PR silently repairs that drift without adding anything that stops it recurring across 8 fields instead of 3.
The drift is user-visible and asymmetric: strict defaults to true in the marketplace-entry schema ("Require the plugin manifest to be present in the plugin folder"), so the marketplace entry's copy is what a user sees while BROWSING the catalog and plugin.json's copy is what they see once INSTALLED — a divergence shows the product's front door describing itself two different ways.
Nothing guards cross-manifest parity. (Corrected during verification: the plugin-validate job in .github/workflows/pr-checks.yml:218-243 DOES run claude plugin validate . --strict and claude plugin validate ./plugins/coder-eval --strict on every PR, so each file is schema-validated on its own and a JSON syntax error does NOT ship. What is unguarded is only the field-equality check between the two manifests — claude plugin validate never compares them.) This directly contradicts the repo's own stated convention — tests/test_action_version_pin.py exists verbatim because "Nothing asserted it, which is how action.yml shipped pinned to 0.8.6 while main was already 0.8.9" — and CLAUDE.md's harness-loop rule ("could a custom lint rule have prevented this?").
Fix: add a ~15-line test beside tests/test_action_version_pin.py that json.loads both files, locates the entry with name == "coder-eval" in plugins, and asserts equality for the shared key set (displayName, description, author, homepage, repository, license, keywords) — deliberately excluding version (marketplace entry has none), source, category, and tags (marketplace-entry-only per the runtime schema). That single test closes the parity gap (the only unguarded half).
Nits
- [Axis 3] Newly pinned $schema URLs are unverified by any test or CI step, and already disagree with the runtime validator over the displayName key this PR adds (
.claude-plugin/marketplace.json:2) — The PR adds"$schema": "https://json.schemastore.org/claude-code-marketplace.json"(marketplace.json:2) and"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json"(plugin.json:2), butgrep -rn 'schemastore\|\$schema' tests/ .github/ Makefilereturns ZERO hits — no test or CI step fetches either URL or validates the manifests against it, so the pin is editor-only and a typo'd or later-dead URL would ship unnoticed. That the two validators already disagree is demonstrable today:claude plugin validate --strictrecognisesdisplayName(a probe with"displaName"emitsUnknown field 'displaName' — did you mean 'displayName'?), yet in both downloaded schemasdisplayNameappears at exactly one path —/properties/channels/items/properties(plugin schema) and/properties/plugins/items/properties/channels/items/properties(marketplace schema) — i.e. neither declaresdisplayNameat the plugin-manifest top level or atplugins[]., so the twodisplayNamekeys this PR adds (marketplace.json:13, plugin.json:4) are outside the pinned schemas' documented property set and pass only becauseadditionalPropertiesis unset in both. I confirmed both files currently validate clean (jsonschema.Draft7Validator→ 0 errors each), so this is a missing guard, not a live breakage. Either add a network-optional test that Draft7-validates each manifest against a vendored copy of its declared$schema(skip on fetch failure, following the repo's derived-pin test style), or drop the$schemalines rather than declaring a contract nothing checks. - [Axis 7] Newly added
keywords/tagsdiscovery vocabulary adds no discovery value — inert for the client search, and internally redundant (near-synonyms, second vocabulary with no plugin.json counterpart) (.claude-plugin/marketplace.json:21) — The PR reworkskeywordsfrom 5 to 7 values and adds a new 5-valuetagsarray (marketplace.json:21-30):
"keywords": ["claude-code-skills","skill-testing","skill-activation","evaluation","benchmark","agent-testing","ci"],
"tags": ["skills", "evals", "github-actions", "sandbox", "llm-judge"]
Neither array is consulted by the client's discovery search. The plugin browse/install filter in Claude Code 2.1.247 is, verbatim from the binary:
l.filter((W)=>W.entry.name.toLowerCase().includes(ae)||W.entry.displayName?.toLowerCase().includes(ae)||W.entry.description?.toLowerCase().includes(ae)||s&&W.marketplaceName.toLowerCase().includes(ae))
Only name, displayName, description and the marketplace name, matched as case-insensitive substrings. Corroborating de-facto evidence: of the 289 plugin entries in the installed claude-plugins-official marketplace, exactly 1 sets keywords, and all 3 that set tags use it as a curation label (["community-managed"]), not as topical keywords.
Two consequences worth recording. First, dropping testing and skills from keywords is NOT a discoverability regression — the field was never searched. Second, the levers this PR moved that DO work are description (mkt:15) and the newly added displayName (mkt:13 / plg:4), and both moved in the right direction: the new description adds the substrings test, trigger, eval suite and ci gate, which the base description did not contain.
Recommendation: keep both arrays (they cost nothing and may be indexed by an out-of-client directory), but do not treat them as the discovery lever, and add a short JSON-adjacent note or commit-message line saying so — otherwise the next contributor tunes keywords expecting search impact it cannot have. If a term must be findable in-client, it has to appear as a substring of description, displayName or name.
3. [Axis 7] Repositioned lede not rippled to the two prose surfaces that mirror the manifest description (plugins/coder-eval/.claude-plugin/plugin.json:6) — The stated intent of this PR (commit 8d7eba9, "lead both manifests with skill evaluation") is to invert the pitch so skill-activation testing comes first. plugin.json:6 and marketplace.json:14 now read "Test whether your Claude Code skills actually trigger — then author, run, and analyze the eval suite that proves it, locally or as a CI gate."
The two prose surfaces that mirrored the old manifest description were not updated and still lead with the old ordering:
plugins/coder-eval/README.md:3-4:Author, run, and analyze [Coder Eval](https://coder-eval.com) suites from inside Claude/Code — including whether your own Claude Code skills actually trigger.— this is the base plugin.json description rendered as prose, verbatim in structure.docs/PLUGIN.md:2-5frontmatter:description: >-/Install the Coder Eval plugin for Claude Code — six slash commands to scaffold,/author, review, run and analyze evaluation suites, including an activation suite/that measures whether your own Claude Code skills actually trigger.
A user who reads the marketplace listing (skill-testing first) then clicks through to the plugin README on GitHub or to coder-eval.com/PLUGIN gets the inverted pitch, with skill activation demoted to a trailing "including..." clause. Since plugins/coder-eval/README.md is one of the four SKILL_DOC_SURFACES (tests/test_custom_lint.py:1238) the repo already treats as a governed surface, the fix is cheap: rewrite the README:3-4 lede and the docs/PLUGIN.md:3-5 frontmatter to lead with skill-activation testing, matching the manifests.
4. [Axis 7] Listing description leads with a capability that needs an out-of-band CLI install and paid credentials, with no hint in the 139 chars available (plugins/coder-eval/.claude-plugin/plugin.json:6) — plugin.json:6 (and its twin marketplace.json:14) reads "Test whether your Claude Code skills actually trigger — then author, run, and analyze the eval suite that proves it, locally or as a CI gate." — an imperative capability claim as the lead. The plugin ships no runtime: four of its six skills shell out to a coder-eval binary that the plugin does not install, and running a suite additionally needs ANTHROPIC_API_KEY and real model spend.
The plugin's own skills treat this as the top user surprise and open by defending against it: plugins/coder-eval/skills/init/SKILL.md:17 — "Run coder-eval --version. Installing this plugin did not install the CLI, and"; plugins/coder-eval/skills/check-skill/SKILL.md:61 — same sentence; plugins/coder-eval/skills/task/SKILL.md:20 — "Installing this plugin did not install the CLI." docs/PLUGIN.md gives it a dedicated ## Prerequisite heading in bold.
This is not a truthfulness defect and it is not a regression (the base description had the same shape), which is why it is filed low. But the marketplace listing is frequently the only text a prospective user reads, and there is budget to say so: at 139 chars this description sits at the measured median (135) across the 147 plugin entries installed under ~/.claude/plugins/marketplaces/, where the max observed is 586. A trailing clause such as "…locally or as a CI gate. Drives the separately installed coder-eval CLI." costs ~45 chars, sets the expectation before install, and — per the search predicate documented in the keywords/tags finding — adds cli and install as searchable substrings for free.
What's Missing
Parallel paths:
- 🟡 Parallel paths 🟡 —
.claude-plugin/marketplace.json'splugins[0]entry gaineddisplayName/author/homepage/repository/licenseso it now hand-mirrorsplugins/coder-eval/.claude-plugin/plugin.jsonon 8 keys (was 3), but no generator or parity test was added in the same change.claude plugin validate --strict(pr-checks.ymlplugin-validate) validates each file against its own schema and never compares the two — I re-ran both locally and both pass, so the schema gate is genuinely blind to this. The 3-key surface had already drifted onkeywordsonorigin/main; the PR repairs that drift silently and widens the surface that can drift next. (trigger: .claude-plugin/marketplace.json) (restates: Axis 1: Manifest metadata duplicated verbatim across marketplace.json and plugin.json with no parity guard) - 🔵 Parallel paths 🔵 — the skills-first lede was not rippled, and the un-rippled set is larger than the two surfaces already filed: beyond
plugins/coder-eval/README.md:3-4anddocs/PLUGIN.md:2-5, the rootREADME.md:100-102plugin pitch ("scaffold a suite, author a task, check whether a skill triggers, read the results" — skill-checking third) anddocs/tutorials/07-plugin-in-claude-code.md:2-4frontmatter ("install the plugin, scaffold a task directory, author and review a task, then read the run" — skill activation absent entirely) both still carry the old ordering. Four prose surfaces, not two. (trigger: plugins/coder-eval/.claude-plugin/plugin.json) (restates: Axis 7: Repositioned lede not rippled to the two prose surfaces that mirror the manifest description) - 🔵 Parallel paths 🔵 — the marketplace-level
description(.claude-plugin/marketplace.json:9, a distinct string from the plugin entry's) was repositioned to lead with skill testing, but its three peer product one-liners were not:pyproject.toml:4("Evaluate, benchmark, and A/B-test AI coding agents…" — the PyPI listing, the highest-traffic install surface),mkdocs.yml:2-5site_description("Evaluate & benchmark AI coding agents and Claude Code skills…" — the coder-eval.com meta description / search snippet) andREADME.md:1's H1 tagline. The GitHub repo description already leads skills-first ("Test that your Claude Code skills, MCP servers, and CLIs actually work…"), so after this PR the manifests + GitHub agree and those three do not. (trigger: .claude-plugin/marketplace.json)
Tests:
- 🔵 Tests 🔵 — no test ties
marketplace.json'sname+plugins[0].nameto the install command five docs surfaces hardcode (README.md:107,plugins/coder-eval/README.md:14,docs/PLUGIN.md:18,docs/USER_GUIDE.md:134,docs/tutorials/07-plugin-in-claude-code.md:41all say/plugin install coder-eval@coder-eval). This PR addsdisplayName: "Coder Eval"right besidename, which makes a follow-up "rename it to match the display name" edit the natural next move — andclaude plugin validate --strictpasses on a rename, so five copy-paste install commands would go stale with no gate firing. (trigger: .claude-plugin/marketplace.json) - 🔵 Tests 🔵 — the two newly pinned
$schemaURLs (marketplace.json:2,plugin.json:2) declare a contract that nothing intests/,Makefileor.github/fetches or validates against; the manifests are schema-checked only byclaude plugin validate --strict, which uses the CLI's own built-in rules, not the pinned schemastore documents. Either vendor the schemas and add a network-optional Draft7 validation test, or drop the pins. (trigger: plugins/coder-eval/.claude-plugin/plugin.json) (restates: Axis 3: Newly pinned $schema URLs are unverified by any test or CI step)
Display & mapping dicts:
- 🔵 Display / mapping 🔵 — the new
displayName: "Coder Eval"(marketplace.json:13,plugin.json:4) becomes the title Claude Code renders in the plugin browser, but every in-repo title surface still renderscoder-eval: the shields badge labelclaude__code__plugin-coder--eval(README.md:5),plugins/coder-eval/README.md:1(# coder-eval — Claude Code plugin) anddocs/PLUGIN.md's H1. CE026 already enforces exactly this class of parity for the GitHub Action (action.ymlname:↔ marketplace slug ↔ badge label); no analogous check exists for the plugin, so the client and the docs can now show two different product names. (trigger: .claude-plugin/marketplace.json)
Downstream consumers:
- 🔵 Downstream consumers 🔵 — the discovery vocabulary now diverges three ways with no shared source: the manifests (7 keywords + 5 tags, this PR),
pyproject.toml:9-16(27 PyPI keywords — hasagent-testing/claude-code-skillsbut not the PR's newskill-testing/skill-activation), and the GitHub repo topics (20 — already carryskill-testing, so they are ahead of PyPI). If the repositioning is real it should movepyproject.toml'skeywordsanddescriptionin the same commit, since PyPI is the surfacepip install coder-evalusers actually search. Note also that the manifests' new vocabulary dropsmcpand anycliterm, which the repo's own GitHub description leads with ("skills, MCP servers, and CLIs"). (trigger: plugins/coder-eval/.claude-plugin/plugin.json) (restates: Axis 7: Newly added keywords/tags discovery vocabulary adds no discovery value)
Daily/nightly:
- 🔵 Daily / nightly 🔵 — blast radius on the two published-surface pipelines is unstated.
claude plugin validate --strictruns only inpr-checks.yml'splugin-validatejob (PR / push / merge_group); the nightlyverify-published-action.yml(cron17 6 * * *) checks tag/pin/PyPI/Marketplace parity for the Action only and never touches the plugin marketplace, so nothing post-merge confirms/plugin marketplace add UiPath/coder_evalstill resolves or that the two schemastore URLs still exist. Two things I verified so the PR body can say them outright: both manifests passclaude plugin validate --strictat HEAD, and release.yml's line-shapedsedfor"version": "X.Y.Z",(plustests/test_action_version_pin.py::_PLUGIN_PIN_PATTERN) still matches afterversionmoved from key #2 to key #4 — so the release bump is unaffected. The eval run path is untouched; nightly eval suites are unaffected. (trigger: .claude-plugin/marketplace.json)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] CE040 — cross-manifest metadata parity (new whole-tree rule). New module
tests/lint/plugin_manifests.py, wired astests/test_custom_lint.py::TestCE040PluginManifestParity(NOT aBaseRuleintests/lint/runner.py— that runner is AST-only over.py; precedent: CE026-CE031, CE033, CE035). Itjson.loads/Users/religa/src/coder_eval/.claude-plugin/marketplace.json, selects theplugins[]entry whosename == "coder-eval",json.loads/Users/religa/src/coder_eval/plugins/coder-eval/.claude-plugin/plugin.json, and asserts equality on the derived shared key set —set(entry) & set(manifest)minus two explicit allowlists (ENTRY_ONLY = {"source", "category", "tags"},MANIFEST_ONLY = {"$schema", "version"}) — rather than a hardcoded 8-name list, so a newly added shared key (the failure mode this PR just demonstrated by growing the surface 3 -> 8) is guarded the day it lands, and a deliberately divergent key must be added to an allowlist with a reason comment. Slots besidetests/test_action_version_pin.py, which already readsplugin.jsonforversionand exists verbatim because "nothing asserted it". Prevents: Finding 1 (medium,.claude-plugin/marketplace.json): 8 byte-identical hand-synced fields with no parity guard. It would have failed onorigin/main, wherekeywordswas already drifted ("skills"vs"claude-code-skills") — the browse-time copy describing the product differently from the installed copy.claude plugin validate --strictin theplugin-validatejob cannot catch this: it validates each file against its own schema and never compares the two. - [ce-lint] CE041 —
$schemapins must be vendored and actually validated (new clause in the sametests/lint/plugin_manifests.py). Vendor the two declared schemas undertests/lint/schemas/claude-code-marketplace.jsonandtests/lint/schemas/claude-code-plugin-manifest.json, keyed by aSCHEMA_URLS: dict[str, Path]map. The rule then: (a) fails if a manifest's$schemavalue is not a key of that map (catches a typo'd or silently swapped URL — nothing intests/,.github/, orMakefilementionsschemastoretoday, so the pin is editor-only decoration); (b) runsjsonschema.Draft7Validator(already a runtime dependency — no new dep, no network) over each manifest against its vendored schema; (c) reports any top-level key the schema does not declare at that level unless it is in anUNDECLARED_KEYS_OKallowlist with a reason string. Clause (c) is what records, in code, thatdisplayName— the key this PR adds atplugins[]and at plugin-manifest top level — is accepted byclaude plugin validate --strictbut appears in neither pinned schema outsidechannels[], and passes today only becauseadditionalPropertiesis unset. Prevents: Finding 2 (low,.claude-plugin/marketplace.json:2): a declared schema contract that nothing checks, plus the demonstrated disagreement between the pinned schemas and the runtime validator overdisplayName. Fully offline and deterministic, so it belongs inmake lint/make verify, not in a network-gated job. - [ce-lint] CE042 — discovery strings must live on the fields that are actually searched (clause 3 of
tests/lint/plugin_manifests.py). Encode the client's real browse/install predicate as data:SEARCHED_FIELDS = ("name", "displayName", "description")(case-insensitive substring match, per the filter observed in Claude Code 2.1.247). The rule then asserts (a) every term in a repo-levelDISCOVERY_TERMStuple (skill,trigger,eval,ci,benchmark) appears as a substring of the concatenated searched fields of BOTH manifests, so a future description rewrite cannot silently drop a term the team believes it is ranking on; (b) everykeywords/tagsvalue that is not such a substring must be listed in anINERT_TERMSallowlist whose comment states that the field is not consulted by the in-client search — the allowlist entry is the warning to the next contributor, delivered at the moment they edit the array; (c)len(description) <= MARKETPLACE_DESCRIPTION_MAX_CHARS(start at 220; current is 141, observed median across installed marketplaces is 135), mirroring theSKILL_LISTING_BUDGET_CHARSprecedent attests/test_custom_lint.py:1249including its "raising the ceiling is allowed in a commit that says why" convention. Prevents: Finding 3 (low,.claude-plugin/marketplace.json:21): 12 newkeywords/tagsstrings that no discovery surface reads, with nothing to stop the next contributor tuning them expecting search impact. Also future-proofs the two levers that DO work (displayName,description) against an unnoticed regression. - [ce-lint] CE044 — the listing lede is SSOT; the prose ledes are derived (generated-surface pattern). Treat
plugins/coder-eval/.claude-plugin/plugin.json'sdescriptionthe waymkdocs.yml'snav:is treated for the index tables (CE028) and the models are forreference/criteria.md(CE033): add amake plugin-ledetarget that writes the manifest description into marker-delimited blocks (<!-- plugin-lede:start -->/<!-- plugin-lede:end -->) inplugins/coder-eval/README.md(the lede at lines 3-4) and thedescription:frontmatter ofdocs/PLUGIN.md(lines 2-5), and a rule that re-renders and diffs — reusing the shared write/diff engine both generated-surface checkers already route through (tests/test_custom_lint.py:2765). If full generation is judged too heavy for two paragraphs, the cheaper mechanical form is: assert the manifest description's first clause (text up to the em dash, normalized to lowercase and stripped of punctuation) is a substring of the README's first two prose lines and of thedocs/PLUGIN.mdfrontmatter description. Prevents: Finding 4 (low,plugins/coder-eval/.claude-plugin/plugin.json:6): commit 8d7eba9 inverted the pitch to lead with skill-activation testing in both manifests, butplugins/coder-eval/README.md:3-4anddocs/PLUGIN.md:2-5still lead with the old ordering — so a user who browses the marketplace and clicks through gets the pitch inverted.plugins/coder-eval/README.mdis already a governedSKILL_DOC_SURFACESentry (tests/test_custom_lint.py:1238), so the surface list needs no new concept. - [ce-lint] CE042 clause (d) — the listing must name the out-of-band prerequisite whenever the skills defend against it. Mechanical and non-semantic: if any
plugins/coder-eval/skills/*/SKILL.mdcontains the defensive sentence fragmentdid not install the CLI(it appears ininit/SKILL.md:17,check-skill/SKILL.md:61,task/SKILL.md:20), then both manifests'descriptionmust match/(coder-eval )?CLI|separately installed/i. The plugin's own skills already treat this as the top user surprise anddocs/PLUGIN.mdgives it a bold## Prerequisiteheading; the rule just forces the same disclosure onto the one surface a prospective user reads before installing, and keeps it true if the prerequisite is ever removed (drop the sentence from the skills and the clause goes quiet on its own). Prevents: Finding 5 (low,plugins/coder-eval/.claude-plugin/plugin.json:6): an imperative capability lede for a plugin that ships no runtime, with ~80 chars of listing budget still unused. Flagged as the most opinionated of the proposals — if the team prefers not to legislate marketing copy, drop this clause and keep CE042 (a)-(c).
Harness improvements (not statically reachable):
- Nightly re-fetch + diff of the vendored
$schemacopies. Add a step to the existing nightlyverify-published-action.yml(which already does published-artifact parity checks on a schedule) thatcurlshttps://json.schemastore.org/claude-code-marketplace.jsonand.../claude-code-plugin-manifest.jsonand diffs each againsttests/lint/schemas/*.json, failing (or opening an issue) on drift or on a non-200. Keep it OUT ofmake verifyso PR CI stays offline and deterministic. Why not static: Needs network and reaches a third-party artifact that changes without a commit here. CE041 can prove the manifests match the vendored schema; only a live fetch can prove the vendored schema still matches what the declared URL serves, or that the URL has not 404'd. Prevents: Finding 2 — the pinned schema URLs are never resolved by anything, so a later-dead or renamed upstream schema would ship unnoticed. - Post-publish marketplace parity check on the served artifact. In the same nightly/post-Release workflow, fetch
https://raw.githubusercontent.com/UiPath/coder_eval/main/.claude-plugin/marketplace.json(what/plugin marketplace add UiPath/coder_evalactually consumes) and re-run the CE040 shared-key comparison against the publishedplugins/coder-eval/.claude-plugin/plugin.jsonfrom the same ref, plus assert the entry'sversion-adjacent pins agree with the released PyPI version. Why not static: A repo-local lint proves the working tree is consistent; it cannot prove the copy users are served is. Release-path seds, a reverted commit, or a hand-edit landing onmainoutside a PR would all pass local lint and still leave the front door describing itself two ways. Prevents: Finding 1 — the drift is user-visible precisely on the published copy (browse-time description/keywords vs. installed plugin.json). make plugin-ledetarget + a CONTRIBUTING runbook line. Ship the generator half of CE044 as a first-class target next tomake docs-indexesandmake plugin-reference, list it in the CLAUDE.md "regenerate a generated surface" block, and add one line to CONTRIBUTING's release/plugin section: changing the listing pitch means runningmake plugin-ledeand re-reading the two prose surfaces. Why not static: The lint rule can only fail the build; the dev-workflow gap is that today there is no one-command way to ripple a pitch change, so the reviewer's suggested fix is a hand-edit in three files — exactly the shape that produced the drift. Prevents: Findings 1 and 4 — both are hand-sync surfaces with no generator.- Pin and record the Claude CLI version the
plugin-validatejob validates with..github/workflows/pr-checks.yml:218-243installs@anthropic-ai/claude-codeunpinned, so the meaning ofclaude plugin validate --strict(and of the browse/search predicate the CE042 discovery clause encodes) silently follows upstream. Install a pinned version, echoclaude --versioninto the job summary, and put a dated comment next toSEARCHED_FIELDSintests/lint/plugin_manifests.pynaming the client version the predicate was read from. Why not static: The searched-field set lives inside a third-party binary and can change between releases; no repo-local check can introspect it. The best available guard is making the assumption dated, attributed, and observable in CI output so the next person can re-verify instead of trusting a stale comment. Prevents: Finding 3 — the whole argument thatkeywords/tagsare inert rests on one observed client build; without a recorded version the conclusion decays into folklore.
Top 5 Priority Actions
- No finding in this review can change a task's score or final_status for identical agent output — the diff is plugin-manifest and docs metadata only, so treat every item below as maintainability work rather than correctness risk.
- Add a ~15-line parity test beside
tests/test_action_version_pin.pythat loads both manifests and asserts the 8 shared keys (name,displayName,description,author,homepage,repository,license,keywords) are equal between.claude-plugin/marketplace.json:12-31andplugins/coder-eval/.claude-plugin/plugin.json:3-19, since this change grows the hand-synced surface from 3 fields to 8 and the pair already drifted onkeywordson main — scope it to field equality, becauseplugin-validatein.github/workflows/pr-checks.yml:218already schema-validates each file on its own. - Ripple the new skill-activation-first lede from
plugins/coder-eval/.claude-plugin/plugin.json:6into the two prose surfaces still carrying the old ordering —plugins/coder-eval/README.md:3-4(a governedSKILL_DOC_SURFACESentry) and thedocs/PLUGIN.md:2-5frontmatter — so a user clicking through from the marketplace listing does not meet the inverted pitch. - Append a short clause naming the separately installed CLI to the shared listing description at
plugins/coder-eval/.claude-plugin/plugin.json:6and.claude-plugin/marketplace.json:15(139 chars used, well under the 586-char observed ceiling), because four of six skills open by defending against exactly this surprise (plugins/coder-eval/skills/init/SKILL.md:17). - Either add a network-optional test that validates each manifest against a vendored copy of its declared schema, or drop the unverified
$schemapins at.claude-plugin/marketplace.json:2andplugins/coder-eval/.claude-plugin/plugin.json:2— neither schema declares thedisplayNamekey this change adds at the level it is used, so the pin passes only becauseadditionalPropertiesis unset. - Record in a commit note or contributor doc that
keywordsandtags(.claude-plugin/marketplace.json:21-30) are inert for in-client discovery — the browse filter matches onlyname,displayName,descriptionand the marketplace name — so the next contributor tunesdescriptioninstead of a field that cannot affect search.
Stats: 0 🔴 · 0 🟠 · 1 🟡 · 4 🔵 across 7 axes reviewed.
Three review items on #141. `claude code skills only?` (tmatup, on the plugin.json description) — no. The harness runs claude-code, codex and antigravity, and the description read as if skill activation were the whole product. Both descriptions now say "...and benchmark any coding agent" before the mechanism clause, so the skill-activation hook still leads without narrowing the tool to it. `tags` alongside `keywords` (review nit 1) — dropped. The two marketplace fields are described near-identically by the schema, held disjoint values with cross-boundary synonyms, and the plugin-manifest schema has no `tags` property at all, so an installed user's copy silently lost those five strings. The three genuinely distinct terms (`github-actions`, `sandbox`, `llm-judge`) are folded into the single `keywords` list both manifests now share; `skills` and `evals` were already covered by `claude-code-skills`/`skill-testing`/`evaluation`. CE044 (review's harness suggestion) — the commit left eight fields as byte-identical duplicates across two files with nothing comparing them; the only test that read plugin.json read its `version`. `tests/lint/plugin_manifest_parity.py` asserts the shared keys agree and fails on any extra discovery key the manifest cannot mirror unless MARKETPLACE_ONLY records why — so the "which list does a new term go in" rule lives in code, and re-adding `tags` fails `make lint`. `claude plugin validate --strict` passes on both files; make lint 376 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed in Acted on@tmatup — "claude code skills only?" (thread) — no, and the description was the reason it read that way. Both descriptions now lead Nit 1 — Harness suggestion — parity test — added as CE044 ( Not acted on, deliberatelyNit 2 — CE045 / vendored schemas / offline validation — real scope, and the closed-world half is the load-bearing part (neither schemastore document sets Prose surfaces still leading with the old order — Rollout skew (the review asked for this to be stated)
Verification
|
…ry metadata The plugin and marketplace manifests are what a plugin directory indexes and what the `/plugin` browser shows before install. Both led with "Author, run, and analyze coder-eval suites" — the mechanism, not the job — so the one thing that distinguishes this plugin (does your Claude Code skill actually trigger?) sat behind an em dash where a truncated listing drops it. - Both descriptions now lead with the skill-activation job. - Keywords lead with the skill terms (`claude-code-skills`, `skill-testing`, `skill-activation`) instead of the generic `evaluation`/`testing` pair. - The marketplace entry gains the discovery metadata the schema supports and it was missing: `displayName`, `author`, `homepage`, `repository`, `license`, `tags`. - `plugin.json` gains `displayName` so the listing shows "Coder Eval" rather than the kebab-case id. Deliberately NOT adding `version` to the marketplace entry: it pins updates, and release.yml's seds maintain only the two existing derived pins (action.yml, plugin.json). A third would go stale and strand users on a cached copy — the exact failure test_action_version_pin.py exists to prevent. `version` stays second in plugin.json with its trailing comma, so _PLUGIN_PIN_PATTERN and release.yml's sed still match. Verified: `claude plugin validate --strict` on both manifests, `make lint`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh
Audited both files field-by-field against the two official SchemaStore schemas (`claude-code-plugin-manifest.json`, `claude-code-marketplace.json`). Only one real gap survived: neither manifest declared `$schema`, so an editor gave no autocomplete and no validation on the files whose typos are otherwise invisible until a directory listing renders wrong. Both URLs resolve 200 and are the ones the plugin reference documents. Everything else the schemas allow is deliberately absent, not overlooked: component-path keys (`skills`, `commands`, `agents`, `hooks`, `mcpServers`, …) because the plugin ships only `skills/`, which is auto-discovered; `strict` because its default of true is the wanted behavior; `defaultEnabled` likewise; `dependencies`/`channels`/`userConfig`/`settings`/`monitors`/`themes`/ `outputStyles` because the plugin has none; marketplace `version`/`metadata`/ `forceRemoveDeletedPlugins`/`allowCrossMarketplaceDependenciesOn` because a single-plugin marketplace served from its own repo needs none of them; and the entry's `version` for the pin-drift reason recorded in the previous commit. Note SchemaStore lags the reference on `displayName` — the docs specify it on both a plugin manifest and a marketplace entry, the schemas do not list it. Neither schema sets `additionalProperties: false`, so it validates clean under both, verified below. Verified: both files VALID against the downloaded schemas via jsonschema, `claude plugin validate --strict` on both, `make lint` exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh
`owner.email` was the one field the marketplace schema documents that this manifest deliberately left blank, for want of a shared address. `coder-eval@uipath.com` is that address, so a plugin directory reviewer or a user with a packaging question has somewhere to write that is not a personal inbox. Verified: `claude plugin validate --strict`, VALID against claude-code-marketplace.json, `make lint` exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh
`owner.email` reached only the marketplace maintainer. A reader who opens `plugin.json` — the file a plugin directory reads and the one that wins under `strict: true` — still found an `author` with no way to make contact. Both `author` objects now carry `coder-eval@uipath.com`, so the address is present whichever manifest a reviewer or user opens. Surveyed every other author/contact field in the repo. Nothing else needs it: `pyproject.toml` already publishes this same address to PyPI, and `action.yml`'s `author:` is a plain GitHub Actions string with no email form. Verified: `claude plugin validate --strict` on both manifests, both VALID against their published schemas, `make lint` exit 0, version-pin test passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh
Three review items on #141. `claude code skills only?` (tmatup, on the plugin.json description) — no. The harness runs claude-code, codex and antigravity, and the description read as if skill activation were the whole product. Both descriptions now say "...and benchmark any coding agent" before the mechanism clause, so the skill-activation hook still leads without narrowing the tool to it. `tags` alongside `keywords` (review nit 1) — dropped. The two marketplace fields are described near-identically by the schema, held disjoint values with cross-boundary synonyms, and the plugin-manifest schema has no `tags` property at all, so an installed user's copy silently lost those five strings. The three genuinely distinct terms (`github-actions`, `sandbox`, `llm-judge`) are folded into the single `keywords` list both manifests now share; `skills` and `evals` were already covered by `claude-code-skills`/`skill-testing`/`evaluation`. CE044 (review's harness suggestion) — the commit left eight fields as byte-identical duplicates across two files with nothing comparing them; the only test that read plugin.json read its `version`. `tests/lint/plugin_manifest_parity.py` asserts the shared keys agree and fails on any extra discovery key the manifest cannot mirror unless MARKETPLACE_ONLY records why — so the "which list does a new term go in" rule lives in code, and re-adding `tags` fails `make lint`. `claude plugin validate --strict` passes on both files; make lint 376 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7e28ca7 to
d63d51b
Compare
|
Rebased onto Why the rebase was needed: no The conflict: Re-verified post-rebase: |
…tivation suite reports recall 0.0 (#143) * fix(plugin): correct the skill-reachability path, and reuse PR #109's measured descriptions Salvages the parts of the closed PR #109 that stand alone — no dependency on its `optimize/` subsystem or `Dataset.split_field`. ## The reachability bug (the reason this branch exists) Six surfaces told users to point `SKILL_SOURCE_PATH` at `.claude/skills`. A local plugin path must be a plugin ROOT — a directory holding `skills/` — so the skill resolves at `<path>/skills/<name>/SKILL.md`. One level too deep loads nothing at all. Every activation suite `check-skill` generates today therefore reports recall 0.0, which the template's own comment calls "reads exactly like a broken skill". `ci` was writing the broken path into users' scheduled CI workflows, where it produces a permanent red indistinguishable from the drift the schedule exists to detect. Verified against the real CLI rather than taken from the closed PR, two layouts over one probe skill at `<root>/.claude/skills/probe-alpha/SKILL.md`: claude --plugin-dir <root>/.claude/skills -> NONE (never loads) claude --plugin-dir <root>/.claude -> `.claude:probe-alpha` Corrected in all six: `check-skill`, `ci`, the `activation.yaml` template, `docs/PLUGIN.md`, and tutorial 07 (guidance + its troubleshooting row, which blamed only an unset variable). ## Bare-name collision hazard (check-skill) `skill_triggered` matches on the bare name and strips `plugin:` prefixes, and Claude Code ships its own unscoped `init`. A colliding `skill_name` does not error — it credits whichever skill fires. Ported PR #109's warning to say so. ## Descriptions Took PR #109's six-skill set. Two carry evidence: `analyze` is its promoted `a-regression` variant (train 1.000 vs 0.667 non-overlapping, test 1.000 vs 0.909) and adds the "what regressed" trigger it deterministically missed; `lint-tasks`' trim was measured at ceiling (F1 1.000 both splits). The `check-skill`/`init`/`task` trims are tighter prose, NOT individually A/B'd — recorded here so nobody later reads them as validated. Listing budget 1,576 -> 1,351 of 1,600. That headroom is the point: the budget is shared with every skill the user has installed. ## Deliberately not taken PR #109's `--split` guidance, its `run_limits`/`setting_sources` template notes, its threshold-currency warning, and its "plan expands the dataset" claim all describe code that PR added. Each was checked against `main` and is absent, so porting the prose would document behavior this tree does not have. Verified: `make lint` exit 0 (371 passed), `claude plugin validate --strict`, `coder-eval plan` on the edited template. `make test` shows 10 pre-existing failures in `tests/test_judge_litellm.py` that reproduce on clean `main` — the repo's own `litellm/` directory shadows the installed package when pytest runs from the root. This diff is Markdown and YAML only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh * fix(test): add CE045, document the plugin-path divergence, unpin a test from ordering Three follow-ups to the skill-reachability fix in this branch's first commit. ## CE045 — a claude-code plugin path must be a plugin root (Authored as CE044; renumbered on rebase — #141's merge claimed that id for the manifest-parity rule, a different check on the same plugin tree.) Repo convention says a fixed bug should become a lint rule when its root cause is mechanically detectable. This one is: `SKILL_SOURCE_PATH` must never name a directory whose last segment is `skills`, because a plugin's skills live at `<path>/skills/<name>/SKILL.md` and one level deeper loads nothing at all. Six surfaces drifted to the wrong value together precisely because nothing held them in agreement. The rule globs the surfaces rather than enumerating them, so a seventh is caught by existing rather than by being remembered. It carries a mutation guard pinning both the rejected and the accepted forms, and it was verified to fail with file and line when the pre-fix value is reintroduced. ## The plugin-path harness divergence `docs/agents/HARNESS_PARITY.md` gains a section for it. claude-code REQUIRES a plugin root; `codex_agent._setup_skills` and `antigravity_agent._resolve_skills_paths` both scan the bare and the nested layout and take whichever holds a `<skill>/SKILL.md`. So `.claude/skills` works on two backends out of three and fails silently on the third — which is how the wrong value survived in six places. Per the parity rule a divergence is either fixed or documented, and this one was neither. That is also why CE045 keys on `SKILL_SOURCE_PATH` and leaves the Codex docs' `PLUGIN_PATH` alone. ## test_install_failure_is_not_latched A latent ordering hazard, found while investigating an unrelated schedule change. It does not fail on today's `-n auto` sharding, and it is not a flake: any change to how tests distribute across workers can surface it, and one did. `signal.signal` was patched module-wide for the test's whole duration, so the async teardown's own `signal.signal(SIGINT, default_int_handler)` restore hit the refusing mock and raised `ValueError` out of teardown — failing the test for something it does not test. The patch is now lifted as soon as the two calls under test are made. The count assertion is also replaced with a comparison of the two calls' signal SETS. A running total of 4 cannot distinguish a latch bug from an unrelated call landing in the same list; the set form tolerates a stray duplicate while still proving the retry, since a latched install would record nothing on the second call. ## Not in this PR The repo's `litellm/` directory is importable as a namespace package, so `import litellm` succeeds and returns an empty module — which is why `tests/test_judge_litellm.py` fails with `ModuleNotFoundError: No module named 'litellm.types'` for anyone without the optional `litellm` extra, instead of skipping. Ten failures on a clean checkout, invisible to CI because CI installs the extra. Deliberately left for a separate change; the work is parked on `fix/litellm-namespace-shadowing`. Verified: `make lint` exit 0 (CE044 and CE045 both green); `make test` shows the same pre-existing litellm failures as clean main and nothing else; ruff check and format clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh * fix(plugin): close the review's verified gaps — regex blind spot, parallel surfaces, runtime guard Every claim below was reproduced before acting on it. Three of them proved my own text wrong. ## CE045's matcher could not see the surface it was written for `[^"'\s]+` stops at the first space, so the GitHub Actions form `SKILL_SOURCE_PATH=${{ github.workspace }}/.claude/skills` captured `${{` — never the path. The `ci` skill, which the rule's own docstring calls the highest-cost surface because it writes into users' SCHEDULED workflows, was structurally unreachable. Worse, a companion test asserted that truncated capture as CORRECT, so the hole was written down as covered. The value now runs to end-of-line or a closing quote, and `${{ ... }}` expressions are stripped before the last-segment check. Verified by reintroducing the pre-fix `ci` line: the rule flags it with file and line. The mutation guard is replaced by `KNOWN_BAD_LINES`, the four verbatim pre-fix source lines, replayed through the FULL extract-then-predicate pipeline. The old guard called the predicate on hand-written strings the matcher could never produce, which is exactly how the matcher stayed unproven. That corpus is now the incident record, so the docstring no longer miscounts it ("tutorial 07 twice" was wrong — one line, verified against origin/main). Two bugs my own new tests then caught: `findall` reports an unmatched alternation group as `""` rather than `None`, so picking "the first non-None group" always selected the empty quoted branch; and the YAML walk labelled fixture paths with `relative_to(REPO_ROOT)`. ## The globs and the tasks walk both under-covered `_GLOBS` missed root-level `*.md`, `.github/workflows/**`, `tasks/**` and `experiments/**`. `test_globs_cover_every_file_naming_the_token` now derives the expected set from `git grep -l`, so "globbed, not enumerated" is true by construction rather than aspirational. A non-vacuity assert fails if the walk ever matches nothing at all. The YAML walk skipped every `$`-bearing value as "covered by the assignment check", which those globs did not in fact cover — so it had zero eligible inputs and could not regress-fail. It now skips only a BARE variable reference and judges the literal tail otherwise, so `$REPO_ROOT/.claude/skills` is caught. It also reads `defaults.agent` and `variants[].agent`, and has a fixture test. ## Three parallel surfaces carried the same bug - `docs/AB_EXPERIMENTS.md` — the canonical "A/B a skill" recipe wrote `path: "../skills"` under `type: claude-code`. Its `with-skill` arm loaded nothing, so the experiment silently compared two identical arms. - `experiments/default.yaml` — the baseline every experiment inherits documented `"path": "/path/to/skills"`. - `experiments/plugin-comparison.yaml` — `$PLUGIN_PATH` under a claude-code default, with no statement of the contract. That last one also falsifies a claim I shipped: HARNESS_PARITY and the rule's docstring both said `PLUGIN_PATH` was "deliberately outside the rule, since a skills directory is valid there". It is not Codex-only. Both now say the exemption is a limit of the rule's scope, not a licence to use the deeper form. `docs/agents/CLAUDE_CODE.md` — the reference a claude-code task author actually reads — states the contract and links the parity section; `CLAUDE.md`'s index of known divergences gains this one as its highest-cost entry. ## The parity probe taught the opposite of its lesson Run from a repo root, `claude --plugin-dir "$(pwd)/.claude/skills"` STILL lists the skill — Claude Code discovers `./.claude/skills/` natively, independent of `--plugin-dir`. An engineer following the documented probe would have concluded the deeper path works. The probe now runs from outside the skill's repo and tells the reader the signal is the NAMESPACE prefix, not mere presence. ## A plugin root loads the whole plugin, not just its skills Verified against the real CLI: a root holding `skills/probe-beta/`, `agents/probe-subagent.md` and `commands/probe-cmd.md` offers all three as `root:probe-beta`, `root:probe-subagent`, `root:probe-cmd`. So pointing a suite at a repo's `.claude` hands the evaluated agent every project subagent and command. With `.claude/agents/pdf-expert.md` present while measuring `pdf-forms`, the agent may delegate instead of calling the skill; `skill_triggered` records `no` and recall drops for a reason unrelated to the skill's description — and the number becomes repo-dependent, so two suites stop being comparable. `check-skill` and the bundled template now prescribe staging a MINIMAL root containing only the skill under test, and `ci` carries the workflow step to build one (a scheduled job inherits no shell state). The whole-tree form is still offered, with the confound stated. ## A runtime guard, because a repo-scoped lint rule cannot reach users `utils.process_plugins` warned only on an UNDEFINED env var. A path that resolves and exists but holds no `skills/` — precisely `.claude/skills` — reached the SDK with no signal at all. Codex and Antigravity already log the equivalent condition; claude-code, the one harness where the depth is fatal, was silent. It now warns, with four tests. That covers the user repos where `/coder-eval:check-skill` actually writes these suites, which CE045 structurally cannot. ## Exception-safe patch scoping `test_install_failure_is_not_latched` lifted its `signal.signal` patch with a bare `monkeypatch.undo()` after the calls. If `ensure_crash_handlers` raised — the regression the test exists to catch — the undo was skipped and the teardown `ValueError` masked the real failure. Now `monkeypatch.context()`, which restores on every exit path. ## Not addressed here The reviewer's namespaced-`skill_name` matching, the sibling tests' process-wide `signal.signal` patches, and the proposed CE046/CE047 meta-rules are real but are their own changes. The migration note for already-emitted user artifacts needs a release decision first. Verified: `make lint` exit 0 (382 passed); `make test` shows the same 10 pre-existing litellm failures as clean main and nothing else; ruff clean; `claude plugin validate --strict`; `coder-eval plan` on the edited template. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh * fix(utils): use explicit concatenation in the plugin-root warning pyright (`reportImplicitStringConcatenation`) rejected the multi-line warning string added in 754805b. It failed two gates — Quality Gate and Windows Smoke, both on the `Type check with pyright` step — and I had not caught it because pyright could not install in this session, so the one gate my change could break was the one gate I never ran. Now matches the explicit `+` style the neighbouring warning in `codex_agent._setup_skills` already uses. Verified with CI's exact extras (`--extra dev --extra uipath --extra codex --extra litellm`): pyright reports 0 errors, 1 pre-existing warning in `antigravity_agent.py:442` that is not from this branch. `make lint` exit 0, `make test` 4369 passed, ruff clean. The third red gate, Evalboard, is unrelated: it failed on `Install dependencies (lockfile-pinned)`, this branch touches no JS or lockfile, and the job passes on main's last three runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Why
The plugin and marketplace manifests are what a plugin directory indexes and what the
/pluginbrowser shows before install. This is groundwork for submitting toanthropics/claude-plugins-community(viaclau.de/plugin-directory-submission) and to the curated community lists.Both descriptions led with "Author, run, and analyze coder-eval suites" — the mechanism, not the job. The one thing that distinguishes this plugin (does your Claude Code skill actually trigger?) sat behind an em dash, where a truncated listing drops it.
What changed
Positioning — both descriptions now lead with the skill-activation job, and keywords lead with the skill terms (
claude-code-skills,skill-testing,skill-activation) instead of the genericevaluation/testingpair.Discovery metadata the schema supports and the marketplace entry lacked:
displayName,author,homepage,repository,license,tags.plugin.jsongainsdisplayNameso listings show "Coder Eval" rather than the kebab-case id.$schemaon both files, pinned to the published SchemaStore schemas (both resolve 200):claude-code-plugin-manifest.jsonclaude-code-marketplace.jsonDeliberate omissions
Audited field-by-field against both schemas. Everything else they allow is absent on purpose:
versionplugin.jsonfirst, then the marketplace entry, then the commit SHA — andplugin.jsonsets it, so an entryversionis never consulted. It would be a third derived pin thatrelease.yml's seds do not maintain, drifting stale in a file readers trust. (An earlier commit message here says it would strand users on a cached copy; that overstated it —plugin.jsonwins, so it is dead metadata, not a live hazard.)skills,commands,agents,hooks,mcpServers,lspServers,outputStyles,themes,monitorsskills/, which is auto-discovered.strict,defaultEnabledtrue) are the wanted behavior.dependencies,channels,userConfig,settingsversion,metadata,forceRemoveDeletedPlugins,allowCrossMarketplaceDependenciesOn"version"stays second inplugin.jsonwith its trailing comma, so_PLUGIN_PIN_PATTERNandrelease.yml's sed still match.Contact address
owner.emailand bothauthorobjects carrycoder-eval@uipath.com, so a plugin-directory reviewer or a user with a packaging question has a route that is not a personal inbox — whichever of the two manifests they open.pyproject.tomlalready publishes the same address to PyPI, so this adds no new public surface.Every other author/contact field in the repo was surveyed:
pyproject.tomlalready matches, andaction.yml'sauthor:is a plain GitHub Actions string with no email form.Known caveat
SchemaStore lags the reference on
displayName— the plugin reference specifies it on both a plugin manifest and a marketplace entry; neither schema lists it. Neither setsadditionalProperties: false, so it validates clean under both. If a directory ever renderscoder-evalinstead of "Coder Eval", that is the reason.Out of scope
No changes to
plugins/coder-eval/README.mdordocs/PLUGIN.md. Both already name skill activation, and rewording them touches CE026's guarded surfaces for no directory-ranking gain.Repo topics were updated separately (not a file change): added
claude-code-plugins-marketplace, the topic the auto-generated plugin lists scrape. The repo is now at GitHub's 20-topic cap.Verification
jsonschema, Draft 2020-12)claude plugin validate --strictpasses on the marketplace and the plugin manifestmake lintexit 0 (371 passed)tests/test_action_version_pin.pypasses🤖 Generated with Claude Code
https://claude.ai/code/session_01V5WZ1BbtygK9YVvcgPfNGh